﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Diagnostics;
using Server.DataBase;
using Server.CustomObjects;
using System.Threading;
using System.ComponentModel;

namespace Server
{
    public class ServerCore
    {
        DBMain cDB;
        Socket cSocket;
        PerformanceCounter cPC_Ram;
        PerformanceCounter cPC_CpuUsage;
        BackgroundWorker cCacheWorker;
        List<CacheItem> cMessageCache;
        Dictionary<Int32,EndPoint> cUserEndPointCache;

        long cMessageCounter = 0;
        byte[] cBuffer = new byte[1024];

        public long CacheCounter { get { return this.cMessageCache.Count; } }

        public long MessageCounter
        {
            get
            {
                return this.cMessageCounter;
            }
            private set
            {
                if (this.cMessageCounter < long.MaxValue)
                {
                    this.cMessageCounter++;
                }
            }
        }
        public long UserCounter
        {
            get
            {
                return this.cDB.User.Count();
            }
        }
        public long SessionCounter
        {
            get
            {
                return this.cDB.Session.Count();
            }
        }
        public float AvailableMemory { get { return this.cPC_Ram.NextValue(); } }
        public float CpuUsage { get { return (Int32)this.cPC_CpuUsage.NextValue(); } }

        public ServerCore()
        {
            // Performance Counter
            this.cPC_Ram = new PerformanceCounter("Memory", "Available MBytes");
            this.cPC_CpuUsage = new PerformanceCounter("Processor", "% Processor Time", "_Total");

            // DataBase Connection
            this.cDB = new DBMain();

            // start Cache
            this.cUserEndPointCache = new Dictionary<int, EndPoint>();
            this.cMessageCache = new List<CacheItem>();
            this.cCacheWorker = new BackgroundWorker();
            this.cCacheWorker.DoWork += cCacheWorker_DoWork;
            this.cCacheWorker.RunWorkerCompleted += (obj, e) => { this.cCacheWorker.RunWorkerAsync(this.cMessageCache); };
            this.cCacheWorker.RunWorkerAsync(this.cMessageCache);

            // start server
            this.cSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            this.cSocket.Bind(new IPEndPoint(IPAddress.Any, 1337));

            SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs();
            socketEventArg.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 1337);
            socketEventArg.SetBuffer(this.cBuffer, 0, this.cBuffer.Length);
            socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(delegate(object s, SocketAsyncEventArgs e)
            {
                this.cSocket.ReceiveFromAsync(socketEventArg);

                if (e.SocketError == SocketError.Success)
                {
                    // retrieve the data from the buffer and convert to messageObj
                    Message _Message = new Message(Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred));

                    if (_Message.GUID != "-1")
	                {
                        if (_Message.SourceID != -1)
                            if (this.cUserEndPointCache.ContainsKey(_Message.SourceID))
                                this.cUserEndPointCache[_Message.SourceID] = e.RemoteEndPoint;
                            else
                                this.cUserEndPointCache.Add(_Message.SourceID, e.RemoteEndPoint);

                        if (Globals.Network.DebugMode)
                            Log.Write(Log.eDirection.Receiving, _Message);
                        
                        this.ProcessMessage(_Message, e.RemoteEndPoint);                        
	                }
                    else
                    {
                        // message invalid
                        Log.Write(Log.eLogType.Server, "Message is invalid" + _Message.ToString());
                        return;
                    }
                    
                }
                else
                {
                    Log.Write(Log.eLogType.Server, e.SocketError.ToString());
                }
            });

            this.cSocket.ReceiveFromAsync(socketEventArg);
        }

        void cCacheWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            List<CacheItem> _CacheItems = (List<CacheItem>)e.Argument;
            if(this.SendCachedMasseges)
            foreach (CacheItem _CacheItem in _CacheItems)
            {
                this.SendMessage(_CacheItem.Message, false);
            }

            Thread.Sleep(3000);
        }

        public Boolean SendMessage(Message pMessage, Boolean pCache = true)
        {
            if (this.cSocket == null) { return false; }
            
            try
            {
                SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs();

                if (pMessage.TargetEndPoint != null)
                {
                    socketEventArg.RemoteEndPoint = pMessage.TargetEndPoint;
                }
                else if (pMessage.TargetID != -1)
                {
                    if (this.cUserEndPointCache.ContainsKey(pMessage.TargetID))
                    {
                        socketEventArg.RemoteEndPoint = this.cUserEndPointCache[pMessage.TargetID];
                    }
                    else
                    {
                        // TODO => insert Message into couldNotBeDeliveredCache
                    }
                    
                }
                else
                {
                    return false;
                }
                    
                byte[] _bMessage = Encoding.UTF8.GetBytes(pMessage.ToString());
                socketEventArg.SetBuffer(_bMessage, 0, _bMessage.Length);
                this.cSocket.SendToAsync(socketEventArg);

                if (Globals.Network.DebugMode)
                    Log.Write(Log.eDirection.Sending, pMessage);

                if (pCache)
                    this.cMessageCache.Add(new CacheItem(pMessage));

                return true;
            }
            catch (Exception e)
            {
                Log.Write(Log.eLogType.Server, e.Message);
                return false;
            }
        }

        public void ProcessMessage(Message pMessage, EndPoint pEndPoint)
        {
            User _User = null;

            #region CachedMessages
            if (pMessage.Command == Message.eCommand.Confirmation || pMessage.Command == Message.eCommand.Decline)
            {
                CacheItem _CacheItem = this.cMessageCache.Find(ci => ci.Message.GUID == pMessage.RequestGUID);

                if (_CacheItem != null)
                {
                    this.cMessageCache.Remove(_CacheItem);
                    switch (_CacheItem.Message.Command)
                    {
                        case Message.eCommand.WantToAddYouAsContact:
                            if (pMessage.Command == Message.eCommand.Confirmation)
                            {
                                this.cDB.Contacts.AcceptFriendship(pMessage.SourceID, pMessage.TargetID);
                                this.SendMessage(new Message(Message.eCommand.ContactAdded, _CacheItem.Message.SourceID, pMessage.SourceID, this.cDB.User.GetUser(_CacheItem.Message.SourceID).Name, "", pEndPoint));                                
                                this.SendMessage(new Message(Message.eCommand.ContactAdded, pMessage.SourceID, _CacheItem.Message.SourceID, this.cDB.User.GetUser(pMessage.SourceID).Name));
                            }
                            else 
                            {
                                this.SendMessage(new Message(Message.eCommand.Confirmation, -1, pMessage.SourceID, "", pMessage.GUID),false);
                                this.cDB.Contacts.DeclineFriendship(pMessage.SourceID, pMessage.TargetID);
                            }
                            break;
                        default:
                            break;
                    }

                    return;
                }
            }
            #endregion

            #region DefaultMessage
            switch (pMessage.Command)
            {
                case Message.eCommand.Debug:
                    // shows Debug on ServerGUI
                    Log.Write(Log.eDirection.Receiving, pMessage);
                    break;

                case Message.eCommand.Text:
                    this.MessageCounter++;
                    this.SendMessage(new Message(Message.eCommand.Text, pMessage.SourceID, pMessage.TargetID, pMessage.Content));
                    break;

                case Server.CustomObjects.Message.eCommand.LogIn:
                    #region LogIn
                    String[] _LoginArray = Globals.SplitMessageContent(pMessage.Content);
                    Int32 _UserID = -1;

                    // validate LoginArray
                    if (_LoginArray.Length < 2 || _LoginArray[0] == "" || _LoginArray[1] == "") { break; }

                    _UserID = cDB.User.verifyUserData(_LoginArray[0], _LoginArray[1], pEndPoint);

                    if (_UserID != -1)
                    {
                        this.SendMessage(new Message(Message.eCommand.Confirmation, -1, _UserID, "", pMessage.GUID, pEndPoint),false);

                        if (this.cUserEndPointCache.ContainsKey(_UserID))
                            this.cUserEndPointCache[_UserID] = pEndPoint;
                        else
                            this.cUserEndPointCache.Add(_UserID, pEndPoint);

                        Log.Write(Log.eLogType.Server, "User: " + _UserID + " logged in");
                    }
                    else
                    {
                        this.SendMessage(new Message(Message.eCommand.Decline, -1, -1, "", pMessage.GUID, pEndPoint),false);
                        Log.Write(Log.eLogType.Server, "LogIn failed => " + pMessage.Content + " From => " + pEndPoint.ToString());
                    }
                    #endregion
                    break;

                case Server.CustomObjects.Message.eCommand.CreateUser:
                    #region CreateUser
                    String[] _NewUserArray = pMessage.Content.Split(new string[] { Globals.Separator.Message }, StringSplitOptions.None);

                    if (_NewUserArray.Length < 2 || _NewUserArray[0] == "" || _NewUserArray[1] == "") { break; }

                    _User = this.cDB.User.AddUser(_NewUserArray[0], _NewUserArray[1], ""); // _NewUserArray[1] pEndPoint                    

                    if (_User.ID != -1)
                    {
                        this.SendMessage(new Message(Message.eCommand.Confirmation, -1, _User.ID, "", pMessage.GUID, pEndPoint));
                        Log.Write(Log.eLogType.Server, "new user created: " + _User.Name);
                    }
                    else
                    {
                        this.SendMessage(new Message(Message.eCommand.Decline, -1, -1, "", pMessage.GUID, pEndPoint));
                        Log.Write(Log.eLogType.Server, "register new user failed => " + pMessage.Content + " From => " + pEndPoint.ToString());
                    }
                    #endregion
                    break;

                case Server.CustomObjects.Message.eCommand.AddContact:
                    #region AddContact
                    this.SendMessage(new Message(Message.eCommand.Confirmation, -1, pMessage.SourceID, "", pMessage.GUID, pEndPoint));

                    String[] _SplittedContent = Globals.SplitMessageContent(pMessage.Content);
                    _User = this.cDB.User.GetUser(_SplittedContent[0]);                    
                    if (_User == null) { return; }

                    if (this.cDB.Contacts.RequestFriendship(pMessage.SourceID, _User.ID))
                    {
                        this.SendMessage(new Message(Message.eCommand.WantToAddYouAsContact, pMessage.SourceID, _User.ID, _User.Name + Globals.Separator.Content + _SplittedContent[1]));       
                    }
                    #endregion
                    break;

                case Message.eCommand.GetSecretKey:
                    // validate public key and send privatekey back
                    if (pMessage.Content == Globals.Security.PublicKey)
                        
                        this.SendMessage(new Message(Message.eCommand.Confirmation, -1, -1, Globals.Security.PrivateKey, pMessage.GUID, pEndPoint), false);
                    break;
                case Message.eCommand.Location:
                    if (pMessage.Content.Contains(Globals.Separator.Content))
                    {
                        String[] _MessageContent = Globals.SplitMessageContent(pMessage.Content);
                        double lat = Math.Round(Convert.ToDouble(_MessageContent[0]), 5);
                        double lng = Math.Round(Convert.ToDouble(_MessageContent[1]), 5);
                        Log.Write(Log.eLogType.Server, "User: " + pMessage.SourceID + " GeoData " + _MessageContent[0] + " & " + _MessageContent[1]);
                        if (cDB.User.UpdateGPS(pMessage.SourceID, _MessageContent[0], _MessageContent[1])) 
                            this.SendMessage(new Message(Message.eCommand.Confirmation, -1, -1,"", pMessage.GUID, pEndPoint), false);
                    
                    }
                    break;
                default:
                    break;
            }
            #endregion
        }

        public void StopResendCache()
        {
            this.SendCachedMasseges = false;
        }
        public void StartResendCache()
        {
            this.SendCachedMasseges = true;
        }

        public bool SendCachedMasseges { get; set; }
    }
}
